// ==UserScript==
// @name         Viper Image URL Copier
// @version      1.0
// @description  Unified full-resolution image URL copier for IMX, Pixhost, Vipr, TurboImageHost, PimpandHost, ImageVenue, ImageBox, ImageBam
// @match        https://viper.to/*
// @match        https://vipergirls.to/*
// @match        https://planetviper.club/*
// @match        https://viperbb.rocks/*
// @match        https://viperkats.eu/*
// @match        https://viperohilia.art/*
// @match        https://viperproxy.org/*
// @match        https://vipervault.link/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @connect      vipr.im
// @connect      imx.to
// @connect      image.imx.to
// @connect      turboimagehost.com
// @connect      turboimg.net
// @connect      imagebam.com
// @connect      images*.imagebam.com
// @connect      imgbox.com
// @connect      images*.imgbox.com
// @connect      pixhost.to
// @connect      www.pixhost.to
// @connect      pixhost.org
// @connect      img*.pixhost.to
// @connect      pimpandhost.com
// @connect      filesor.com
// @connect      imagevenue.com
// @connect      cdno-data.imagevenue.com
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';
    const CONFIG = {
        maxConcurrent: 20,
        requestTimeout: 10000,
        retryAttempts: 2,
        retryDelay: 1000,
        batchDelay: 480
    };
    const HOSTERS = {
        VIPR: { name: 'VIPR', test: /vipr\.im/i, buttonColor: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', workingColor: 'linear-gradient(135deg, #3B82F6 0%, #1D4ED8 100%)', resolve: resolveViprLink, needsFetching: true, extractor: extractViprLinks },
        IMX: { name: 'IMX', test: /imx\.to/i, buttonColor: 'linear-gradient(135deg, #fbbf24 0%, #f97316 100%)', workingColor: 'linear-gradient(135deg, #fb923c 0%, #f97316 100%)', resolve: resolveImxLink, needsFetching: false, extractor: extractImxLinks },
        TURBO: { name: 'TURBO', test: /turboimagehost\.com/i, buttonColor: 'linear-gradient(135deg, #34d399 0%, #059669 100%)', workingColor: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', resolve: resolveTurboLink, needsFetching: true, extractor: extractTurboLinks },
        IMGBAM: { name: 'IMGBAM', test: /imagebam\.com/i, buttonColor: 'linear-gradient(135deg, #fad0c4 0%, #ff9a9e 100%)', workingColor: 'linear-gradient(135deg, #fcb69f 0%, #ff7e5f 100%)', resolve: resolveImagebamLink, needsFetching: true, extractor: extractImagebamLinks },
        IMGBOX: { name: 'IMGBOX', test: /imgbox\.com/i, buttonColor: 'linear-gradient(135deg, #ffb6c1 0%, #dc143c 100%)', workingColor: 'linear-gradient(135deg, #ff7e5f 0%, #dc143c 100%)', resolve: resolveImgboxLink, needsFetching: true, extractor: extractImgboxLinks },
        PIXHOST: { name: 'PIXHOST', test: /pixhost\.(to|org)/i, buttonColor: 'linear-gradient(135deg, #667eea, #764ba2, #f093fb, #f5576c, #fee140, #fa709a, #4facfe, #00f2fe)', workingColor: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)', resolve: resolvePixhostLink, needsFetching: true, extractor: extractPixhostLinks },
        PIMPANDHOST: { name: 'PIMPANDHOST', test: /pimpandhost\.com|filesor\.com/i, buttonColor: 'linear-gradient(135deg, #ec4899 0%, #db2777 100%)', workingColor: 'linear-gradient(135deg, #be185d 0%, #9d174d 100%)', resolve: resolvePimpandhostLink, needsFetching: false, extractor: extractPimpandhostLinks },
        IMAGEVENUE: { name: 'IMAGEVENUE', test: /imagevenue\.com/i, buttonColor: 'linear-gradient(135deg, #1e3a8a 0%, #0d9488 100%)', workingColor: 'linear-gradient(135deg, #1e40af 0%, #0f766e 100%)', resolve: resolveImagevenueLink, needsFetching: true, extractor: extractImagevenueLinks }
    };
    GM_addStyle(`
.vgcopy1-btn{font-size:11px;margin-left:8px;padding:2px 8px;color:white;border:none;border-radius:4px;cursor:pointer;font-family:inherit;font-weight:bold;box-shadow:0 2px 4px rgba(0,0,0,0.2);transition:all 0.2s;display:inline-flex;align-items:center;gap:4px;}
.vgcopy1-btn:hover{transform:translateY(-1px);box-shadow:0 4px 8px rgba(0,0,0,0.3);}
.vgcopy1-btn:active{transform:translateY(0);}
.vgcopy1-btn.working{cursor:wait;}
.vgcopy1-btn.working::after{content:'';width:8px;height:8px;border:2px solid #ffffff;border-radius:50%;border-top-color:transparent;animation:spin 1s linear infinite;}
.vgcopy1-btn.success-pulse{animation:successPulse 2s ease;}
.vgcopy1-cancel-btn{font-size:10px;margin-left:4px;padding:0 4px;background:#6b7280;color:white;border:none;border-radius:2px;cursor:pointer;font-family:inherit;opacity:0.7;transition:opacity 0.2s;}
.vgcopy1-cancel-btn:hover{opacity:1;background:#ef4444;}
.vgcopy1-notification{position:fixed;top:20px;right:20px;background:#10B981;color:white;padding:12px 20px;border-radius:8px;z-index:999999;animation:slideIn 0.3s ease;box-shadow:0 4px 12px rgba(0,0,0,0.15);max-width:300px;font-family:-apple-system,BlinkMacSystemFont,sans-serif;}
.vgcopy1-notification.warning{background:#F59E0B;}
.vgcopy1-notification.error{background:#EF4444;}
.vgcopy1-notification.info{background:#3B82F6;}
.vgcopy1-error-notification{position:fixed;top:80px;right:20px;background:#dc2626;color:white;padding:15px 20px 15px 20px;border-radius:8px;z-index:1000000;animation:slideIn 0.3s ease;box-shadow:0 4px 12px rgba(0,0,0,0.2);max-width:500px;max-height:300px;overflow-y:auto;font-family:-apple-system,BlinkMacSystemFont,sans-serif;}
.vgcopy1-error-notification .error-title{font-weight:bold;margin-bottom:8px;font-size:14px;display:flex;justify-content:space-between;align-items:center;}
.vgcopy1-error-notification .error-content{font-size:12px;line-height:1.4;margin-bottom:10px;white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word;background:rgba(0,0,0,0.1);padding:8px;border-radius:4px;max-height:200px;overflow-y:auto;}
.vgcopy1-error-notification .error-close-btn{position:absolute;top:10px;right:10px;background:rgba(255,255,255,0.2);color:white;border:none;border-radius:50%;width:24px;height:24px;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
.vgcopy1-error-notification .error-close-btn:hover{background:rgba(255,255,255,0.3);}
@keyframes spin{to{transform:rotate(360deg);}}
@keyframes successPulse{0%{box-shadow:0 0 0 0 rgba(16,185,129,0.7);}50%{box-shadow:0 0 0 8px rgba(16,185,129,0);}100%{box-shadow:0 0 0 0 rgba(16,185,129,0);}}
@keyframes slideIn{from{transform:translateX(100%);opacity:0;}to{transform:translateX(0);opacity:1;}}
`);
    const activeOperations = new Map();
    const resolutionCache = new Map();
    let currentErrorNotification = null;
    function showNotification(message, type = 'info', duration = 3000) {
        const notification = document.createElement('div');
        notification.className = `vgcopy1-notification ${type}`;
        notification.textContent = message;
        document.body.appendChild(notification);
        if (duration > 0) {
            setTimeout(() => {
                notification.style.opacity = '0';
                notification.style.transition = 'opacity 0.3s';
                setTimeout(() => notification.remove(), 300);
            }, duration);
        }
        return notification;
    }
    function showErrorNotification(message, failedLinks = []) {
        if (currentErrorNotification && currentErrorNotification.parentElement) {
            const errorContent = currentErrorNotification.querySelector('.error-content');
            const title = currentErrorNotification.querySelector('.error-title span');
            if (title) title.textContent = message;
            if (errorContent && failedLinks.length > 0) {
                const existingText = errorContent.textContent;
                const numberedLinks = failedLinks.map((link, idx) => `${idx + 1}. ${link}`).join('\n\n');
                errorContent.textContent = existingText + (existingText ? '\n\n' : '') + numberedLinks;
            }
            return currentErrorNotification;
        } else {
            const notification = document.createElement('div');
            notification.className = 'vgcopy1-error-notification';
            const titleDiv = document.createElement('div');
            titleDiv.className = 'error-title';
            const titleSpan = document.createElement('span');
            titleSpan.textContent = '⚠️ ' + message;
            const closeBtn = document.createElement('button');
            closeBtn.className = 'error-close-btn';
            closeBtn.title = 'Close';
            closeBtn.textContent = '✕';
            closeBtn.addEventListener('click', () => {
                notification.style.opacity = '0';
                notification.style.transition = 'opacity 0.3s';
                setTimeout(() => {
                    if (notification.parentElement) notification.remove();
                    if (currentErrorNotification === notification) currentErrorNotification = null;
                }, 300);
            });
            titleDiv.appendChild(titleSpan);
            titleDiv.appendChild(closeBtn);
            notification.appendChild(titleDiv);
            if (failedLinks.length > 0) {
                const errorContent = document.createElement('div');
                errorContent.className = 'error-content';
                const numberedLinks = failedLinks.map((link, idx) => `${idx + 1}. ${link}`).join('\n\n');
                errorContent.textContent = numberedLinks;
                notification.appendChild(errorContent);
            }
            document.body.appendChild(notification);
            currentErrorNotification = notification;
            return notification;
        }
    }
    async function copyToClipboard(text) {
        try {
            await GM_setClipboard(text);
            return true;
        } catch (err) {
            showNotification('❌ Clipboard access failed', 'error', 3000);
            return false;
        }
    }
    function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
    function fixProtocolRelativeUrl(url) {
        if (url && url.startsWith('//')) return 'https:' + url;
        return url;
    }
    function isImageUrl(url) {
        if (!url) return false;
        try {
            const parsed = new URL(url, 'https://example.com');
            if (!['http:', 'https:'].includes(parsed.protocol)) return false;
            return /\.(jpg|jpeg|png|gif|webp)$/i.test(parsed.pathname);
        } catch {
            return false;
        }
    }
    function extractViprLinks(post) {
        const links = [];
        try {
            const anchorTags = post.querySelectorAll('a[href*="vipr.im"]');
            anchorTags.forEach(a => {
                const href = a.href;
                if (href && HOSTERS.VIPR.test.test(href)) links.push(href);
            });
        } catch (e) {}
        return [...new Set(links)];
    }
    function extractImxLinks(post) {
        const links = new Set();
        try {
            const anchorTags = post.querySelectorAll('a[href*="imx.to"]');
            anchorTags.forEach(a => {
                const href = a.href;
                if (href && HOSTERS.IMX.test.test(href) && isImageUrl(href)) links.add(href);
            });
            const imgTags = post.querySelectorAll('img[src*="imx.to"]');
            imgTags.forEach(img => {
                const src = img.src;
                if (src && HOSTERS.IMX.test.test(src) && isImageUrl(src)) links.add(src);
            });
            const postText = post.textContent || '';
            if (postText.includes('imx.to')) {
                const bbcodeRegex = /\[IMG\](https?:\/\/[^\s\[\]]*?imx\.to[^\s\[\]]*?)\[\/IMG\]/gi;
                let match;
                while ((match = bbcodeRegex.exec(postText)) !== null) {
                    const url = match[1];
                    if (isImageUrl(url)) links.add(url);
                }
                const urlRegex = /(https?:\/\/[^\s\[\]]*?imx\.to[^\s\[\]]*?\.(?:jpg|jpeg|png|gif|webp))/gi;
                while ((match = urlRegex.exec(postText)) !== null) links.add(match[1]);
            }
        } catch (e) {}
        return Array.from(links);
    }
    function extractTurboLinks(post) {
        const links = [];
        try {
            const anchorTags = post.querySelectorAll('a[href*="turboimagehost.com"]');
            anchorTags.forEach(a => {
                const href = a.href;
                if (href && HOSTERS.TURBO.test.test(href)) links.push(href);
            });
        } catch (e) {}
        return [...new Set(links)];
    }
    function extractImagebamLinks(post) {
        const links = [];
        try {
            const anchorTags = post.querySelectorAll('a[href*="imagebam.com"]');
            anchorTags.forEach(a => {
                const href = a.href;
                if (href && HOSTERS.IMGBAM.test.test(href)) {
                    if (href.includes('/view/') || href.includes('/image/')) links.push(href);
                }
            });
        } catch (e) {}
        return [...new Set(links)];
    }
    function extractImgboxLinks(post) {
        const links = [];
        try {
            const anchorTags = post.querySelectorAll('a[href*="imgbox.com"]');
            anchorTags.forEach(a => {
                const href = a.href;
                if (href && HOSTERS.IMGBOX.test.test(href)) {
                    if (!isImageUrl(href)) links.push(href);
                }
            });
        } catch (e) {}
        return [...new Set(links)];
    }
    function extractPixhostLinks(post) {
        const links = [];
        try {
            const anchorTags = post.querySelectorAll('a[href*="pixhost"]');
            anchorTags.forEach(a => {
                const href = a.href;
                if (href && /pixhost\.(to|org)/i.test(href)) {
                    if (href.includes('/show/')) links.push(href);
                }
            });
        } catch (e) {}
        return [...new Set(links)];
    }
            function extractPimpandhostLinks(post) {
        const links = new Set();
        try {
            const imgTags = post.querySelectorAll('img[src*="filesor.com"]');
            imgTags.forEach(img => {
                const src = img.src;
                if (src && src.includes('filesor.com')) {
                    links.add(src);
                }
            });
        } catch (e) {}
        return Array.from(links);
    }
    function extractImagevenueLinks(post) {
        const links = new Set();
        try {
            const anchors = post.querySelectorAll('a[href*="imagevenue.com"]');
            anchors.forEach(a => {
                const href = a.href;
                if (href && HOSTERS.IMAGEVENUE.test.test(href)) links.add(href);
            });
        } catch (e) {}
        return Array.from(links);
    }
    async function resolveViprLink(link, abortSignal, retryCount = 0) {
        return new Promise((resolve, reject) => {
            if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
            const abortHandler = () => { try { request.abort(); } catch (e) {} };
            const cleanup = () => { abortSignal.removeEventListener('abort', abortHandler); };
            const request = GM_xmlhttpRequest({
                method: "GET", url: link,
                headers: { "Accept": "text/html", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": link },
                timeout: CONFIG.requestTimeout,
                onload: function(response) {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (response.status !== 200) {
                        if (retryCount < CONFIG.retryAttempts) {
                            setTimeout(() => { resolve(resolveViprLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                        } else { reject(new Error(`HTTP ${response.status}: ${link}`)); }
                        return;
                    }
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    let imageUrl = null;
                    const anchorTags = doc.querySelectorAll('a[href]');
                    for (const a of anchorTags) {
                        const href = a.href;
                        if (href && href.includes('vipr.im') && isImageUrl(href)) { imageUrl = href; break; }
                    }
                    if (!imageUrl) {
                        const imgTags = doc.querySelectorAll('img[src]');
                        for (const img of imgTags) {
                            const src = img.src;
                            if (src && src.includes('vipr.im') && isImageUrl(src)) { imageUrl = src; break; }
                        }
                    }
                    if (!imageUrl) {
                        const metaTags = doc.querySelectorAll('meta[content]');
                        for (const meta of metaTags) {
                            const content = meta.getAttribute('content');
                            if (content && content.includes('vipr.im') && isImageUrl(content)) { imageUrl = content; break; }
                        }
                    }
                    if (imageUrl) { resolve(fixProtocolRelativeUrl(imageUrl)); }
                    else if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveViprLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`No image URL found in: ${link}`)); }
                },
                onerror: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveViprLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Network error: ${link}`)); }
                },
                ontimeout: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveViprLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Timeout: ${link}`)); }
                }
            });
            abortSignal.addEventListener('abort', abortHandler);
        });
    }
    async function resolveImxLink(link) {
        if (!HOSTERS.IMX.test.test(link)) throw new Error(`Not an IMX URL: ${link}`);
        if (link.includes('imx.to/upload/small/')) return link.replace('imx.to/upload/small/', 'image.imx.to/u/i/');
        if (link.includes('/u/t/')) return link.replace('/u/t/', '/u/i/');
        if (link.includes('image.imx.to/u/t/')) return link.replace('/u/t/', '/u/i/');
        if (link.includes('/u/i/')) return link;
        throw new Error(`Unrecognized IMX.TO pattern: ${link}`);
    }
    async function resolveTurboLink(link, abortSignal, retryCount = 0) {
        return new Promise((resolve, reject) => {
            if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
            const abortHandler = () => { try { request.abort(); } catch (e) {} };
            const cleanup = () => { abortSignal.removeEventListener('abort', abortHandler); };
            const request = GM_xmlhttpRequest({
                method: "GET", url: link,
                headers: { "Accept": "text/html", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": link },
                timeout: CONFIG.requestTimeout,
                onload: function(response) {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (response.status !== 200) {
                        if (retryCount < CONFIG.retryAttempts) {
                            setTimeout(() => { resolve(resolveTurboLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                        } else { reject(new Error(`HTTP ${response.status}: ${link}`)); }
                        return;
                    }
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    let imageUrl = null;
                    const imgLink = doc.getElementById('img');
                    if (imgLink) {
                        const imgInside = imgLink.querySelector('img');
                        if (imgInside && imgInside.src && imgInside.src.includes('turboimg.net')) imageUrl = imgInside.src;
                    }
                    if (!imageUrl) {
                        const imgObject = doc.getElementById('img_object');
                        if (imgObject && imgObject.src && imgObject.src.includes('turboimg.net')) imageUrl = imgObject.src;
                    }
                    if (!imageUrl) {
                        const showImageDiv = doc.getElementById('show_image');
                        if (showImageDiv) {
                            const img = showImageDiv.querySelector('img[src*="turboimg.net"]');
                            if (img && img.src) imageUrl = img.src;
                        }
                    }
                    if (!imageUrl) {
                        const imgTags = doc.querySelectorAll('img[src*="turboimg.net"]');
                        for (const img of imgTags) {
                            if (img.src) { imageUrl = img.src; break; }
                        }
                    }
                    if (!imageUrl) {
                        const anchorTags = doc.querySelectorAll('a[href*="turboimg.net"]');
                        for (const a of anchorTags) {
                            if (a.href && isImageUrl(a.href)) { imageUrl = a.href; break; }
                        }
                    }
                    if (imageUrl) { resolve(fixProtocolRelativeUrl(imageUrl)); }
                    else if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveTurboLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`No image URL found in: ${link}`)); }
                },
                onerror: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveTurboLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Network error: ${link}`)); }
                },
                ontimeout: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveTurboLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Timeout: ${link}`)); }
                }
            });
            abortSignal.addEventListener('abort', abortHandler);
        });
    }
        async function resolveImagebamLink(link, abortSignal, retryCount = 0) {
        return new Promise((resolve, reject) => {
            if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
            const abortHandler = () => { try { request.abort(); } catch (e) {} };
            const cleanup = () => { abortSignal.removeEventListener('abort', abortHandler); };
            const request = GM_xmlhttpRequest({
                method: "GET", url: link,
                headers: { "Accept": "text/html", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": link },
                timeout: CONFIG.requestTimeout,
                onload: function(response) {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (response.status !== 200) {
                        if (retryCount < CONFIG.retryAttempts) {
                            setTimeout(() => { resolve(resolveImagebamLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                        } else { reject(new Error(`HTTP ${response.status}: ${link}`)); }
                        return;
                    }
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    let imageUrl = null;
                    const ogImageMeta = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
                    if (ogImageMeta) {
                        const content = ogImageMeta.getAttribute('content');
                        if (content && content.includes('imagebam.com') && isImageUrl(content)) imageUrl = content;
                    }
                    if (!imageUrl) {
                        const imgTags = doc.querySelectorAll('img[src*="imagebam.com"]');
                        for (const img of imgTags) {
                            const src = img.src;
                            if (src && isImageUrl(src)) {
                                if (src.includes('_o.')) { imageUrl = src; break; }
                                else if (!imageUrl) imageUrl = src;
                            }
                        }
                    }
                    if (!imageUrl) {
                        const anchorTags = doc.querySelectorAll('a[href*="imagebam.com"]');
                        for (const a of anchorTags) {
                            const href = a.href;
                            if (href && isImageUrl(href)) {
                                if (href.includes('_o.')) { imageUrl = href; break; }
                                else if (!imageUrl) imageUrl = href;
                            }
                        }
                    }
                    if (imageUrl) { resolve(fixProtocolRelativeUrl(imageUrl)); }
                    else if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveImagebamLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`No image URL found in: ${link}`)); }
                },
                onerror: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveImagebamLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Network error: ${link}`)); }
                },
                ontimeout: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveImagebamLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Timeout: ${link}`)); }
                }
            });
            abortSignal.addEventListener('abort', abortHandler);
        });
    }
    async function resolveImgboxLink(link, abortSignal, retryCount = 0) {
        return new Promise((resolve, reject) => {
            if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
            const abortHandler = () => { try { request.abort(); } catch (e) {} };
            const cleanup = () => { abortSignal.removeEventListener('abort', abortHandler); };
            const request = GM_xmlhttpRequest({
                method: "GET", url: link,
                headers: { "Accept": "text/html", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": link },
                timeout: CONFIG.requestTimeout,
                onload: function(response) {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (response.status !== 200) {
                        if (retryCount < CONFIG.retryAttempts) {
                            setTimeout(() => { resolve(resolveImgboxLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                        } else { reject(new Error(`HTTP ${response.status}: ${link}`)); }
                        return;
                    }
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    let imageUrl = null;
                    const ogImageMeta = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
                    if (ogImageMeta) {
                        const content = ogImageMeta.getAttribute('content');
                        if (content && content.includes('imgbox.com') && isImageUrl(content)) imageUrl = content;
                    }
                    if (!imageUrl) {
                        const mainImg = doc.getElementById('img');
                        if (mainImg && mainImg.src && mainImg.src.includes('imgbox.com')) imageUrl = mainImg.src;
                    }
                    if (!imageUrl) {
                        const imgTags = doc.querySelectorAll('img[src*="imgbox.com"]');
                        for (const img of imgTags) {
                            const src = img.src;
                            if (src && src.includes('_o.') && isImageUrl(src)) { imageUrl = src; break; }
                        }
                    }
                    if (!imageUrl) {
                        const anchorTags = doc.querySelectorAll('a[href*="imgbox.com"]');
                        for (const a of anchorTags) {
                            const href = a.href;
                            if (href && href.includes('_o.') && isImageUrl(href)) { imageUrl = href; break; }
                        }
                    }
                    if (!imageUrl) {
                        const imgTags = doc.querySelectorAll('img[src*="imgbox.com"]');
                        if (imgTags.length > 0) imageUrl = imgTags[0].src;
                    }
                    if (imageUrl) { resolve(fixProtocolRelativeUrl(imageUrl)); }
                    else if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveImgboxLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`No image URL found in: ${link}`)); }
                },
                onerror: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveImgboxLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Network error: ${link}`)); }
                },
                ontimeout: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveImgboxLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Timeout: ${link}`)); }
                }
            });
            abortSignal.addEventListener('abort', abortHandler);
        });
    }
    async function resolvePixhostLink(link, abortSignal, retryCount = 0) {
        return new Promise((resolve, reject) => {
            if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
            const abortHandler = () => { try { request.abort(); } catch (e) {} };
            const cleanup = () => { abortSignal.removeEventListener('abort', abortHandler); };
            const request = GM_xmlhttpRequest({
                method: "GET", url: link,
                headers: { "Accept": "text/html", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": link },
                timeout: CONFIG.requestTimeout,
                onload: function(response) {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (response.status !== 200) {
                        if (retryCount < CONFIG.retryAttempts) {
                            setTimeout(() => { resolve(resolvePixhostLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                        } else { reject(new Error(`HTTP ${response.status}: ${link}`)); }
                        return;
                    }
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    let imageUrl = null;
                    const ogImageMeta = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
                    if (ogImageMeta) {
                        const content = ogImageMeta.getAttribute('content');
                        if (content && content.includes('pixhost') && isImageUrl(content)) imageUrl = content;
                    }
                    if (!imageUrl) {
                        const possibleIds = ['image', 'img', 'picture', 'photo', 'show_image'];
                        for (const id of possibleIds) {
                            const imgElement = doc.getElementById(id);
                            if (imgElement && imgElement.src && imgElement.src.includes('pixhost')) { imageUrl = imgElement.src; break; }
                        }
                    }
                    if (!imageUrl) {
                        const commonContainers = doc.querySelectorAll('[id*="image"], [id*="img"], [class*="image"], [class*="img"]');
                        for (const container of commonContainers) {
                            const img = container.querySelector('img[src*="pixhost"]');
                            if (img && img.src) { imageUrl = img.src; break; }
                        }
                    }
                    if (!imageUrl) {
                        const imgTags = doc.querySelectorAll('img[src*="pixhost"]');
                        for (const img of imgTags) {
                            const src = img.src;
                            if (src && isImageUrl(src)) {
                                if (!src.includes('thumbs') && !src.includes('_thumb') && !src.includes('_small') && !src.includes('_mini')) {
                                    imageUrl = src; break;
                                }
                            }
                        }
                    }
                    if (!imageUrl) {
                        const imgTags = doc.querySelectorAll('img[src*="pixhost"]');
                        if (imgTags.length > 0) {
                            for (const img of imgTags) {
                                if (!img.src.includes('thumbs')) { imageUrl = img.src; break; }
                            }
                            if (!imageUrl) imageUrl = imgTags[0].src;
                        }
                    }
                    if (!imageUrl) {
                        const anchorTags = doc.querySelectorAll('a[href*="pixhost"]');
                        for (const a of anchorTags) {
                            const href = a.href;
                            if (href && isImageUrl(href) && !href.includes('thumbs')) { imageUrl = href; break; }
                        }
                    }
                    if (imageUrl) { resolve(fixProtocolRelativeUrl(imageUrl)); }
                    else if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolvePixhostLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`No image URL found in: ${link}`)); }
                },
                onerror: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolvePixhostLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Network error: ${link}`)); }
                },
                ontimeout: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolvePixhostLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Timeout: ${link}`)); }
                }
            });
            abortSignal.addEventListener('abort', abortHandler);
        });
    }
        async function resolvePimpandhostLink(link, abortSignal, retryCount = 0) {
        if (link.includes('filesor.com')) {
            return link.replace(/_[sl](\.[^.]+)$/i, '$1');
        }
        throw new Error(`Cannot resolve Pimpandhost page links due to Cloudflare protection: ${link}`);
    }
    async function resolveImagevenueLink(link, abortSignal, retryCount = 0) {
        return new Promise((resolve, reject) => {
            if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
            const abortHandler = () => { try { request.abort(); } catch (e) {} };
            const cleanup = () => { abortSignal.removeEventListener('abort', abortHandler); };
            const request = GM_xmlhttpRequest({
                method: "GET", url: link,
                headers: { "Accept": "text/html", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": link },
                timeout: CONFIG.requestTimeout,
                onload: function(response) {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (response.status !== 200) {
                        if (retryCount < CONFIG.retryAttempts) {
                            setTimeout(() => { resolve(resolveImagevenueLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                        } else { reject(new Error(`HTTP ${response.status}: ${link}`)); }
                        return;
                    }
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    let imageUrl = null;
                    const mainImage = doc.getElementById('main-image');
                    if (mainImage && mainImage.src && mainImage.src.includes('imagevenue.com')) imageUrl = mainImage.src;
                    if (!imageUrl) {
                        const imgTags = doc.querySelectorAll('img[src*="cdno-data.imagevenue.com"]');
                        for (const img of imgTags) {
                            const src = img.src;
                            if (src && src.includes('cdno-data.imagevenue.com') && isImageUrl(src)) { imageUrl = src; break; }
                        }
                    }
                    if (!imageUrl) {
                        const ogImage = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
                        if (ogImage && ogImage.content) {
                            const content = ogImage.content;
                            if (content.includes('imagevenue.com') && isImageUrl(content)) imageUrl = content;
                        }
                    }
                    if (!imageUrl) {
                        const text = doc.body.textContent;
                        const regex = /(https?:\/\/[^\s"]*cdno-data\.imagevenue\.com[^\s"]*\.(?:jpg|jpeg|png|gif|webp))/i;
                        const match = text.match(regex);
                        if (match) imageUrl = match[1];
                    }
                    if (imageUrl) { resolve(fixProtocolRelativeUrl(imageUrl)); }
                    else if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveImagevenueLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`No image URL found in: ${link}`)); }
                },
                onerror: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveImagevenueLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Network error: ${link}`)); }
                },
                ontimeout: function() {
                    cleanup();
                    if (abortSignal.aborted) { reject(new Error('Operation cancelled')); return; }
                    if (retryCount < CONFIG.retryAttempts) {
                        setTimeout(() => { resolve(resolveImagevenueLink(link, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                    } else { reject(new Error(`Timeout: ${link}`)); }
                }
            });
            abortSignal.addEventListener('abort', abortHandler);
        });
    }
    async function resolveAllLinks(links, hoster, abortController, progressCallback) {
        if (!hoster.needsFetching) {
            const results = [];
            for (let i = 0; i < links.length; i++) {
                try {
                    results.push(await hoster.resolve(links[i]));
                } catch (error) {
                    results.push({ error: error.message, link: links[i] });
                }
                if (progressCallback) progressCallback(i + 1, links.length);
            }
            return results;
        }
        const results = [], batches = [], abortSignal = abortController.signal;
        const uncachedLinks = [];
        for (const link of links) {
            if (resolutionCache.has(link)) {
                results.push(resolutionCache.get(link));
            } else {
                uncachedLinks.push(link);
            }
        }
        if (progressCallback && results.length > 0) progressCallback(results.length, links.length);
        for (let i = 0; i < uncachedLinks.length; i += CONFIG.maxConcurrent) {
            batches.push(uncachedLinks.slice(i, i + CONFIG.maxConcurrent));
        }
        for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
            if (abortSignal.aborted) break;
            const batch = batches[batchIndex];
            const batchPromises = batch.map(link =>
                hoster.resolve(link, abortSignal).then(result => {
                    resolutionCache.set(link, result);
                    return result;
                }).catch(error => ({ error: error.message, link }))
            );
            const batchResults = await Promise.all(batchPromises);
            results.push(...batchResults);
            if (progressCallback && !abortSignal.aborted) progressCallback(results.length, links.length);
            if (batchIndex < batches.length - 1 && !abortSignal.aborted) await sleep(CONFIG.batchDelay);
        }
        return results;
    }
    function addButtonToPost(post) {
        for (const [hosterName, hoster] of Object.entries(HOSTERS)) {
            try {
                if (post.querySelector(`.vgcopy1-btn[data-hoster="${hosterName}"]`)) continue;
                const links = hoster.extractor(post);
                if (links.length === 0) continue;
                const nodeControls = post.querySelector('span.nodecontrols');
                if (!nodeControls) continue;
                const button = document.createElement('button');
                button.className = 'vgcopy1-btn';
                button.dataset.hoster = hosterName;
                button.innerHTML = `<b>${hoster.name}</b> (${links.length})`;
                button.title = `Copy ${links.length} full-resolution image URLs`;
                button.style.background = hoster.buttonColor;
                const cancelButton = document.createElement('button');
                cancelButton.className = 'vgcopy1-cancel-btn';
                cancelButton.textContent = '✕';
                cancelButton.title = 'Cancel operation';
                cancelButton.style.display = 'none';
                const postId = (post.id || `post_${Date.now()}`) + '_' + hosterName;
                button.addEventListener('click', async function(event) {
                    event.preventDefault(); event.stopPropagation();
                    if (!navigator.onLine) {
                        showNotification('❌ You appear to be offline', 'error', 3000);
                        return;
                    }
                    if (button.classList.contains('working')) {
                        const operation = activeOperations.get(postId);
                        if (operation && operation.abortController) operation.abortController.abort();
                        return;
                    }
                    const originalText = button.innerHTML, originalBackground = button.style.background;
                    const abortController = hoster.needsFetching ? new AbortController() : null;
                    if (hoster.needsFetching) {
                        activeOperations.set(postId, { abortController, button, cancelButton, originalText, originalBackground, wasCancelled: false });
                    }
                    try {
                        button.classList.add('working');
                        button.style.background = hoster.workingColor;
                        button.innerHTML = `<b>${hoster.name}</b> (0/${links.length})`;
                        if (hoster.needsFetching) cancelButton.style.display = 'inline';
                        showNotification(`Resolving ${links.length} ${hoster.name} image URLs...`, 'info', 2000);
                        const resolved = await resolveAllLinks(links, hoster, abortController, (current, total) => {
                            button.innerHTML = `<b>${hoster.name}</b> (${current}/${total})`;
                        });
                        if (hoster.needsFetching) {
                            const operation = activeOperations.get(postId);
                            if (!operation || operation.wasCancelled) {
                                cleanupOperation(postId); return;
                            }
                            cleanupOperation(postId);
                        } else {
                            button.classList.remove('working');
                            button.style.background = originalBackground;
                            cancelButton.style.display = 'none';
                        }
                        const successful = [...new Set(resolved.filter(r => typeof r === 'string'))];
                        const failed = resolved.filter(r => r && r.error);
                        if (successful.length === 0) {
                            button.innerHTML = originalText;
                            button.style.background = originalBackground;
                            showNotification(`❌ All ${links.length} URLs failed to resolve`, 'error', 4000);
                            const failedUrls = failed.map(f => f.link);
                            showErrorNotification(`All ${links.length} links failed to resolve`, failedUrls);
                            return;
                        }
                        const textToCopy = successful.join('\n');
                        const copied = await copyToClipboard(textToCopy);
                        if (!copied) return;
                        button.classList.add('success-pulse');
                        setTimeout(() => button.classList.remove('success-pulse'), 2000);
                        const successCount = successful.length;
                        button.innerHTML = `<b>${hoster.name}</b> (${successCount})`;
                        button.style.background = originalBackground;
                        if (failed.length > 0) {
                            showNotification(`✅ Copied ${successCount}/${links.length} image URLs`, 'info', 3000);
                            const failedUrls = failed.map(f => f.link);
                            showErrorNotification(`${failed.length} links failed to resolve`, failedUrls);
                        } else {
                            showNotification(`✅ Copied ${successCount} image URLs to clipboard`, 'info', 3000);
                        }
                    } catch (error) {
                        if (hoster.needsFetching) {
                            cleanupOperation(postId);
                        } else {
                            button.classList.remove('working');
                            button.style.background = originalBackground;
                            button.innerHTML = originalText;
                            cancelButton.style.display = 'none';
                        }
                        if (!error.message.includes('cancelled')) {
                            showNotification(`❌ Failed: ${error.message}`, 'error', 4000);
                        }
                    }
                });
                cancelButton.addEventListener('click', function(event) {
                    event.preventDefault(); event.stopPropagation();
                    const operation = activeOperations.get(postId);
                    if (operation && operation.abortController) {
                        operation.wasCancelled = true;
                        operation.abortController.abort();
                        cleanupOperation(postId);
                        showNotification('⏹️ Operation cancelled', 'info', 2000);
                    }
                });
                function cleanupOperation(postId) {
                    const operation = activeOperations.get(postId);
                    if (operation) {
                        operation.button.classList.remove('working');
                        operation.button.innerHTML = operation.originalText;
                        operation.button.style.background = operation.originalBackground;
                        if (operation.cancelButton) operation.cancelButton.style.display = 'none';
                        activeOperations.delete(postId);
                    }
                }
                nodeControls.appendChild(button);
                if (hoster.needsFetching) nodeControls.appendChild(cancelButton);
            } catch (e) {}
        }
    }
    function initialize() {
        const posts = document.querySelectorAll('li.postbitlegacy');
        posts.forEach(post => addButtonToPost(post));
        if (posts.length > 0) {
            let scanTimeout = null;
            const observer = new MutationObserver(() => {
                if (scanTimeout) clearTimeout(scanTimeout);
                scanTimeout = setTimeout(() => {
                    document.querySelectorAll('li.postbitlegacy:not(.vgcopy1-processed)').forEach(post => {
                        post.classList.add('vgcopy1-processed');
                        addButtonToPost(post);
                    });
                }, 150);
            });
            observer.observe(document.body, { childList: true, subtree: true });
        }
        window.addEventListener('beforeunload', () => {
            activeOperations.forEach((operation, postId) => {
                if (operation.abortController) operation.abortController.abort();
                if (operation.button) {
                    operation.button.classList.remove('working');
                    if (operation.cancelButton) operation.cancelButton.style.display = 'none';
                }
            });
            activeOperations.clear();
        });
    }
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();